// MIDISOng class holds all information about a song
// And all the tracks of different instruments

import java.io.File;
import java.nio.ByteBuffer;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.Sequence;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Track;
import javax.sound.midi.*;

int timeResolution;

class MidSong {
  final  int NOTE_ON = 0x90;
  final  int NOTE_OFF = 0x80;
  final  String[] NOTE_NAMES = {
    "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
  };
  ArrayList<MIDITrack> tracks;
  PVector timeSignature;
  Sequence sequence;
  int trackNumber;
  Instrument[] orchestra;
  int tempo;
  int pulse;

  MidSong() {
    tracks = new ArrayList<MIDITrack>();
    loadInstruments();
    trackNumber = 0;
    timeSignature = new PVector(4, 4);
  }

  void loadSong(String name) {
    removeSong();
    sequence  = null;
    try {
      sequence = MidiSystem.getSequence(new File(dataPath("")+"/songs/"+name));
      timeResolution = sequence.getResolution();
      readSong();
    }
    catch(Exception e) {
      println(e);
    }
  }

  void loadSongFP(String name) {
    removeSong();
    sequence  = null;
    try {
      sequence = MidiSystem.getSequence(new File(name));
      currentTrackName = name.substring(name.lastIndexOf('/')+1, name.length());
      timeResolution = sequence.getResolution();
      readSong();
    }
    catch(Exception e) {
      println(e);
    }
  }

  void removeSong() {
    tracks.clear();
    trackNumber = 0;
    timeSignature = new PVector(4, 4);
  }

  void readSong() {
    println("Reading MID song...");
    for (Track track :  sequence.getTracks()) {
      tracks.add(new MIDITrack());
      if (timeSignature != null) {
        tracks.get(trackNumber).timeSignature = timeSignature;
      }
      println("Track " + trackNumber + ": size = " + track.size());
      //println();
      for (int i=0; i < track.size(); i++) {
        MidiEvent event = track.get(i);
        if (debug) {
          print("@" + event.getTick() + " ");
        }
        MidiMessage message = event.getMessage();
        processMessage(message, event.getTick());
      }
      println();
    }
    removeEmptyTracks();
    trackList.clear();
    for (int i = 0; i < tracks.size(); i++) {
      //println(tracks.get(i).instrument.getName());
      //println(tracks.get(i).name);
      String name = tracks.get(i).name;
      if (name == null || name.equals("")){
        name = tracks.get(i).instrument.getName();
      }
      ListBoxItem lbi = trackList.addItem(name, i);
    }
    println("Song read! recorded "+ tracks.size()+" tacks.");
  }

  void processMessage(MidiMessage message, long tick) {
    if (message instanceof MetaMessage) {
      processMetaMessage(message);
    }
    else if (message instanceof ShortMessage) {
      processShortMessage(message, tick);
    }
    else {
      if (debug) {
        println("@ Other message: " + message.getClass());
      }
    }
  }

  void processMetaMessage(MidiMessage message) {
    MetaMessage mm = (MetaMessage) message;
    String tmp;
    switch(mm.getType()) {
    case 1:
      tmp = new String(mm.getData());
      println("Sequence number: " + tmp);
      break;     
    case 2:
      tmp = new String(mm.getData());
      println("Text: " + tmp);
      break;        
    case 3:
      tmp = new String(mm.getData());
      println("Track name: " + tmp);
      tracks.get(trackNumber).name = tmp;
      break;
    case 4:
      tmp = new String(mm.getData());
      println("Instrument: " + tmp);
      tracks.get(trackNumber).instrumentName = tmp;
      break;
    case 6:
      tmp = new String(mm.getData());
      println("Marker: " + tmp);
      tracks.get(trackNumber).instrumentName = tmp;
      break;      

    case 81:
      byte[] buf = mm.getData();
      int r = (buf[2] & 0xFF) | ((buf[1] & 0xFF) << 8) | ((buf[0] & 0x0F) << 16);
      int bpm = 60000000 / r; 
      tempo = bpm;
      break;
    case 88:
      byte[] buf2 = mm.getData();
      println("@ Time signature: " + buf2[0] +"/"+ buf2[1]*buf2[1]);
      println("@ Metronome pulse : " + buf2[2]);
      println("@ 32th notes per MIDI-quarter note : " + buf2[3]);
      tracks.get(trackNumber).timeSignature = new PVector(buf2[0], buf2[1]*buf2[1]);
      timeSignature = new PVector(buf2[0], buf2[1]*buf2[1]);
      pulse = buf2[3];
      break;
    case 89:
      byte[] buf3 = mm.getData();
      println("@ Key signature: " + buf3[0] +" sharps and "+ buf3[1]+" flats");

      break;
    case 47:
      println("@ End of track");
      trackNumber++;
      break;
    default:
      print("@ Other Meta message: " + mm.getClass());
      println(" Type: " + mm.getType());
      break;
    }
  }

  void processShortMessage(MidiMessage message, long tick) {
    ShortMessage sm = (ShortMessage) message;
    //print("Channel: " + sm.getChannel() + " ");
    if (sm.getCommand() == NOTE_ON) {
      int key = sm.getData1();
      int octave = (key / 12)-1;
      int note = key % 12;
      String noteName = NOTE_NAMES[note];
      int velocity = sm.getData2();
      if (velocity > 0) {
        if (debug) {
          println("Note on, " + noteName + octave + " key=" + key + " velocity: " + velocity);
          println();
        }
        tracks.get(trackNumber).addNote(noteName+octave, key, velocity, tick);
      }
      else {
        if (debug) {

          println("Note off, " + noteName + octave + " key=" + key + " velocity: " + velocity);
          println();
        }
        tracks.get(trackNumber).closeNote(noteName+octave, key, velocity, tick);
      }
    } 
    else if (sm.getCommand() == NOTE_OFF) {
      int key = sm.getData1();
      int octave = (key / 12)-1;
      int note = key % 12;
      String noteName = NOTE_NAMES[note];
      int velocity = sm.getData2();
      if (debug) {
        println("Note off, " + noteName + octave + " key=" + key + " velocity: " + velocity);
        println();
      }
      tracks.get(trackNumber).closeNote(noteName+octave, key, velocity, tick);
    } 
    else if (sm.getCommand() >= 192 && sm.getCommand() <= 207) {
      int ch = sm.getCommand() - 192 + 1;

      println("@ "+ orchestra[sm.getData1()] + " to channel "+ ch);
      tracks.get(trackNumber).instrumentNumber = sm.getData1(); 
      tracks.get(trackNumber).instrument = orchestra[sm.getData1()];
    }
    else {
      if (debug) {
        println("Other Command:" + sm.getCommand());
      }
    }
  }


  void removeEmptyTracks() {
    for (int i = 0; i < tracks.size(); i++) {
      if (tracks.get(i).closedNotes.size() == 0) {
        tracks.remove(i);
        removeEmptyTracks();
      }
    }
  }

  void loadInstruments() {
    Synthesizer synthesizer = null;
    try {
      synthesizer = MidiSystem.getSynthesizer();
      synthesizer.open();
    }             
    catch(Exception e) {
      println("Unable to create sythesizer object");
    }
    orchestra = synthesizer.getAvailableInstruments();
  }
}

